﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;

namespace Td.Additional.Environment
{
    /// <summary>
    /// Command line parameter parser.
    /// </summary>
    public class CommandLineParser
    {
        #region Properties

        /// <summary>
        /// Gets or sets the parameter list.
        /// </summary>
        /// <value>The parameter list.</value>
        public Dictionary<String, CommandLineParameter> Parameters { get; private set; }
        /// <summary>
        /// Gets or sets a value indicating whether [case sensitivity].
        /// </summary>
        /// <value><c>true</c> if [case sensitivity]; otherwise, <c>false</c>.</value>
        public Boolean CaseSensitivity { get; set; }
        /// <summary>
        /// Gets or sets the parameter prefix.
        /// </summary>
        /// <value>The parameter prefix.</value>
        public String ParameterPrefix { get; set; }
        /// <summary>
        /// Gets or sets the value prefix.
        /// </summary>
        /// <value>The value prefix.</value>
        public Char ValuePrefix { get; set; }
        /// <summary>
        /// Gets or sets the value delimiter.
        /// </summary>
        /// <value>The value delimiter.</value>
        public Char ValueDelimiter { get; set; }
        /// <summary>
        /// Gets or sets the parameter object.
        /// </summary>
        /// <value>The parameter object.</value>
        public Object ParameterObject { get; set; }
        /// <summary>
        /// Gets or sets the error.
        /// </summary>
        /// <value>The error.</value>
        public String Error { get; private set; }
        /// <summary>
        /// Gets or sets the arguments.
        /// </summary>
        /// <value>The arguments.</value>
        public String[] Arguments { get; set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandLineParser"/> class.
        /// </summary>
        public CommandLineParser()
        {
            Parameters = new Dictionary<string, CommandLineParameter>();
            Arguments = System.Environment.GetCommandLineArgs();
            CaseSensitivity = false;
            ParameterPrefix = "/";
            ValuePrefix = ':';
            ValueDelimiter = ';';
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets a value indicating whether this instance is valid.
        /// </summary>
        /// <value><c>true</c> if this instance is valid; otherwise, <c>false</c>.</value>
        public Boolean IsValid
        {
            get
            {
                if (ParameterPrefix == ValuePrefix.ToString() ||
                    ParameterPrefix == ValueDelimiter.ToString() ||
                    ValueDelimiter == ValuePrefix)
                    return false;
                return true;
            }
        }

        /// <summary>
        /// Gets a value indicating whether [parameters valid].
        /// </summary>
        /// <value><c>true</c> if [parameters valid]; otherwise, <c>false</c>.</value>
        public Boolean ParametersValid
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// Fills the parameter class.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        public Boolean FillParameterClass(object obj)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");
            if (!CreateParameterListForType(obj.GetType()))
                return false;
            if (!IsValid)
                return false;
            Dictionary<String, CommandLineParameter> foundParameters = new Dictionary<string, CommandLineParameter>();
            // Check command line content
            for (int i = 0; i < Arguments.Length; i++)
            {
                var paramStr = GetParameter(Arguments[i]);
                if (String.IsNullOrEmpty(paramStr))
                {
                    Error += paramStr + ": Parameter excepted." + System.Environment.NewLine;
                    return false;// Parameter excepted
                }
                CommandLineParameter parameter = null;
                if (!Parameters.TryGetValue(paramStr.ToUpper(CultureInfo.CurrentCulture), out parameter))
                {
                    Error += paramStr + ": Unknown parameter." + System.Environment.NewLine;
                    return false;// Unknown parameter
                }
                if (CaseSensitivity && paramStr != parameter.Name)
                {
                    Error += paramStr + ": False casing." + System.Environment.NewLine;
                    return false;// False casing
                }
                foundParameters.Add(parameter.Name, parameter);
                if (parameter.ValueOption == ValueOption.None)
                    SetValue(parameter, obj, true);
                else
                {
                    var valueCount = 0;
                    if (ValuePrefix == ' ')
                        i++;
                    do
                    {
                        var valueStr = GetValue(Arguments[i], valueCount);
                        if (String.IsNullOrEmpty(valueStr))
                        {
                            if (valueCount == 0 &&
                                (parameter.ValueOption == ValueOption.Required ||
                                parameter.ValueOption == ValueOption.RequiredMultiple))
                            {
                                Error += parameter.Name + ": Missing value." + System.Environment.NewLine;
                                return false;
                            }
                            break;
                        }
                        valueCount++;
                        object value = null;
                        try
                        {
                            if (parameter.Type.IsGenericType)
                                value = Convert.ChangeType(valueStr, parameter.Type.GetGenericArguments()[0]);
                            else
                                value = Convert.ChangeType(valueStr, parameter.Type);
                        }
                        catch
                        {
                            Error += parameter.Name + ": Invalid parameter value type (" + valueStr + ")" + System.Environment.NewLine;
                            return false;// Invalid parameter value type
                        }
                        SetValue(parameter, obj, value);
                        if (parameter.ValueOption == ValueOption.OptionalMultiple ||
                            parameter.ValueOption == ValueOption.RequiredMultiple)
                        {
                            if (ValuePrefix == ' ')
                                i++;
                        }
                        else
                            break;
                    } while (true);
                    if (ValuePrefix == ' ')
                        i--;
                }
            }

            // Check required parameters and set default values
            foreach (var parameter in Parameters)
            {
                // TODO: What is with required, but missing parameters, who havea given default value?
                if (parameter.Value.Option == ParameterOption.Required)
                    if (!foundParameters.ContainsKey(parameter.Value.Name))
                    {
                        Error += parameter.Value.Name + ": Is required but missing." + System.Environment.NewLine;
                        return false;
                    }
                if (parameter.Value.DefaultValue != null && parameter.Value.FoundValues == 0)
                    SetValue(parameter.Value, obj, parameter.Value.DefaultValue);
            }

            // Check parameter validity.
            foreach (var found in foundParameters)
            {
                foreach (var required in found.Value.RequiredParameters)
                {
                    if (!foundParameters.ContainsKey(required))
                    {
                        Error += found.Key + ": Required parameter '" + required + "' is missed." + System.Environment.NewLine;
                        return false;
                    }
                }
            }
            ParameterObject = obj;
            return true;
        }

        /// <summary>
        /// Gets the help lines.
        /// </summary>
        /// <returns></returns>
        public List<String> GetHelpLines()
        {
            var maxLineWidth = 79;
            var result = new List<String>();
            var firstLine = String.Empty;
            var parameterColumnWidth = 0;
            foreach (var parameter in Parameters)
            {
                var width = (parameter.Value.ShortName + "," + parameter.Value.Name).Length;
                if (width > parameterColumnWidth)
                    parameterColumnWidth = width;
            }
            parameterColumnWidth += 2;
            foreach (var parameter in Parameters)
            {
                if (!String.IsNullOrEmpty(firstLine))
                    firstLine += " ";
                var line = ParameterPrefix + parameter.Value.Name;
                var value = String.Empty;
                if (parameter.Value.ValueOption != ValueOption.None)
                {
                    var valueType = "value";
                    if (parameter.Value.Type == typeof(int))
                        valueType = "int";
                    if (parameter.Value.Type == typeof(String))
                        valueType = "String";
                    if (parameter.Value.Type == typeof(Boolean))
                        valueType = "Bool";
                    value = valueType;
                    if (parameter.Value.ValueOption == ValueOption.OptionalMultiple ||
                        parameter.Value.ValueOption == ValueOption.RequiredMultiple)
                        value += "[" + ValueDelimiter + valueType + ValueDelimiter + "...]";
                    value = ValuePrefix + value;
                    if (parameter.Value.ValueOption == ValueOption.Optional ||
                        parameter.Value.ValueOption == ValueOption.OptionalMultiple)
                        value = "[" + value + "]";
                    line += value;
                }
                if (parameter.Value.Option == ParameterOption.Optional)
                    line = "[" + line + "]";
                firstLine += line;
                line = parameter.Value.ShortName;
                if (!String.IsNullOrEmpty(line))
                    line += ",";
                var columnWidth = (line + parameter.Value.Name).Length;
                line = "  " + line + parameter.Value.Name + Repeat(" ", parameterColumnWidth - columnWidth);
                // Add description, wrap if nessecary
                foreach (var wrapLine in WrapLine(parameter.Value.Description, maxLineWidth -(parameterColumnWidth + 2)))
                {
                    result.Add(line + wrapLine);
                    line = Repeat(" ", parameterColumnWidth + 2);
                }
                line = String.Empty;
                line += "This parameter is " + parameter.Value.Option.ToString() + " and ";
                switch (parameter.Value.ValueOption)
                {
                    case ValueOption.None:
                        line += "has no value.";
                        break;
                    case ValueOption.Optional:
                        line += "has an optional value.";
                        break;
                    case ValueOption.OptionalMultiple:
                        line += "have multiple optinal values.";
                        break;
                    case ValueOption.Required:
                        line += "requires a value.";
                        break;
                    case ValueOption.RequiredMultiple:
                        line += "requires at least one value.";
                        break;
                    default:
                        break;
                }
                foreach (var wrapLine in WrapLine(line, maxLineWidth - (parameterColumnWidth + 2)))
                    result.Add(Repeat(" ", parameterColumnWidth + 2) + wrapLine);
                if (parameter.Value.DefaultValue != null)
                {
                    line = String.Empty;
                    line += "The default value is '" + parameter.Value.DefaultValue.ToString() + "'.";
                    foreach (var wrapLine in WrapLine(line, maxLineWidth - (parameterColumnWidth + 2)))
                        result.Add(Repeat(" ", parameterColumnWidth + 2) + wrapLine);
                }
                if (parameter.Value.RequiredParameters.Count > 0)
                {
                    line = String.Empty;
                    foreach (var required in parameter.Value.RequiredParameters)
                    {
                        if (!String.IsNullOrEmpty(line))
                            line += ", ";
                        line += required;
                    }
                    line =
                        "This parameter requires the following parameters: " +
                        line;
                    foreach (var wrapLine in WrapLine(line, maxLineWidth - (parameterColumnWidth + 2)))
                        result.Add(Repeat(" ", parameterColumnWidth + 2) + wrapLine);
                }
            }
            var filename = System.Environment.CommandLine.Trim();
            if (filename.LastIndexOf('\\') > 0)
                filename = filename.Substring(filename.LastIndexOf('\\') + 1);
            firstLine = filename.Replace("\"", "") + " " + firstLine;
            result.Insert(0, firstLine);
            result.Add("Strings with spaces must be enclosed with quotes (\"...\")");
            return result;
        }

        /// <summary>
        /// Gets the help text.
        /// </summary>
        /// <returns></returns>
        public string GetHelp()
        {
            var help = String.Empty;
            foreach (var line in GetHelpLines())
            {
                if (!String.IsNullOrEmpty(help))
                    help += System.Environment.NewLine;
                help += line;
            }
            return help;
        }

        /// <summary>
        /// Prints the help.
        /// </summary>
        public void PrintHelp()
        {
            foreach (var line in GetHelpLines())
                Console.WriteLine(line);
        }

        #endregion

        #region Private helpers

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <param name="obj">The obj.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        private Boolean SetValue(CommandLineParameter parameter, Object obj, Object value)
        {
            if (parameter.Type.IsGenericType)
            {
                var list = obj.GetType().InvokeMember(parameter.PropertyName, BindingFlags.GetProperty, null, obj, null);
                // TODO: Create generic list, if invoke returns null ?
                list.GetType().InvokeMember("Add", BindingFlags.InvokeMethod, null, list, new object[] { value });
            }
            else
                obj.GetType().InvokeMember(parameter.PropertyName, BindingFlags.SetProperty, null, obj, new object[] { value });
            parameter.FoundValues++;
            return true;
        }

        /// <summary>
        /// Gets the value.
        /// </summary>
        /// <param name="arg">The argument.</param>
        /// <param name="valueCount">The value count.</param>
        /// <returns>The return value.</returns>
        private String GetValue(String arg, int valueCount)
        {
            if (arg.StartsWith(ParameterPrefix))
                if (ValuePrefix == ' ')
                    return null;
                else
                {
                    var index = arg.IndexOf(ValuePrefix);
                    if (index < 0)
                        return null;
                    arg = arg.Substring(index + 1);
                }
            var result = arg;
            var argCount = 0;
            if (arg.IndexOf(ValueDelimiter) > 0)
            {
                var quote = false;
                result = String.Empty;
                for (int i = 0; i < arg.Length; i++)
                {
                    if (arg[i] == '"')
                        quote = !quote;
                    else
                        if (arg[i] == ValueDelimiter)
                        {
                            if (quote)
                                result += arg[i];
                            else
                            {
                                if (argCount == valueCount)
                                    break;
                                result = String.Empty;
                                argCount++;
                            }
                        }
                        else
                            result += arg[i];
                }
            }
            if (argCount < valueCount)
                result = String.Empty;
            return result;
        }

        /// <summary>
        /// Gets the parameter.
        /// </summary>
        /// <param name="arg">The arg.</param>
        /// <returns></returns>
        private String GetParameter(String arg)
        {
            if (!arg.StartsWith(ParameterPrefix))
                return null;
            var result = arg.Substring(ParameterPrefix.Length);
            var index = result.IndexOf(ValuePrefix);
            if (index > 0)
                result = result.Substring(0, index);
            return result;
        }

        /// <summary>
        /// Wraps the line.
        /// </summary>
        /// <param name="line">The line.</param>
        /// <param name="width">The width.</param>
        /// <returns>The result lines.</returns>
        private List<String> WrapLine(String line, int width)
        {
            var result = new List<String>();
            var index = 0;
            while (line.Length > 0)
            {
                if (width >= line.Length)
                {
                    result.Add(line);
                    break;
                }
                var count = width;
                while (count > 0 && line[count] != ' ')
                    count--;
                if (count == 0)
                    count = width;
                result.Add(line.Substring(0, count));
                index += count;
                line = line.Substring(count).Trim();
            }
            return result;
        }

        /// <summary>
        /// Repeats the specified string count times.
        /// </summary>
        /// <param name="stringToRepeat">The string to repeat.</param>
        /// <param name="count">The count.</param>
        /// <returns>The result string.</returns>
        private String Repeat(String stringToRepeat, int count)
        {
            var result = String.Empty;
            for (int i = 0; i < count; i++)
                result += stringToRepeat;
            return result;
        }

        /// <summary>
        /// Creates the type of the parameter list for.
        /// </summary>
        /// <param name="type">The type.</param>
        private Boolean CreateParameterListForType(Type type)
        {
            Parameters.Clear();
            Error = String.Empty;
            var defaultAttribute = new CommandLineParameterAttribute();
            foreach (var property in type.GetProperties())
            {
                var attributes = property.GetCustomAttributes(typeof(CommandLineParameterAttribute), true);
                if (attributes == null || attributes.Length == 0)
                    continue;
                var parameter = new CommandLineParameter();
                parameter.Type = property.PropertyType;
                parameter.Name = property.Name;
                parameter.PropertyName = property.Name;
                foreach (var attribute in attributes)
                {
                    var attr = attribute as CommandLineParameterAttribute;
                    if (attr.DefaultValue != defaultAttribute.DefaultValue)
                    {
                        try
                        {
                            object value = null;
                            if (parameter.Type.IsGenericType)
                                value = Convert.ChangeType(attr.DefaultValue, parameter.Type.GetGenericArguments()[0]);
                            else
                                value = Convert.ChangeType(attr.DefaultValue, parameter.Type);
                            parameter.DefaultValue = value;
                        }
                        catch
                        {
                            if (parameter.DefaultValue == null || parameter.DefaultValue.GetType() != parameter.Type)
                                Error += property.Name + ": Type of default value is departing from parameter value type." + System.Environment.NewLine;
                        }
                    }
                    if (attr.ValueOption != defaultAttribute.ValueOption)
                    {
                        parameter.ValueOption = attr.ValueOption;
                        if (parameter.ValueOption == ValueOption.OptionalMultiple ||
                            parameter.ValueOption == ValueOption.RequiredMultiple)
                            if (!property.PropertyType.IsGenericType)
                                Error += property.Name + " is not a generic list." + System.Environment.NewLine;
                    }
                    if (attr.Name != defaultAttribute.Name)
                        parameter.Name = attr.Name;
                    if (attr.Option != defaultAttribute.Option)
                        parameter.Option = attr.Option;
                    if (attr.ShortName != defaultAttribute.ShortName)
                        parameter.ShortName = attr.ShortName;
                    if (attr.Description != defaultAttribute.Description)
                        parameter.Description = attr.Description;
                    if (attr.Requires != defaultAttribute.Requires)
                        parameter.RequiredParameters.AddRange(attr.Requires.Split(new Char[] { ',' }));
                }
                attributes = property.GetCustomAttributes(typeof(DescriptionAttribute), true);
                if (attributes != null && attributes.Length > 0)
                    parameter.Description = (attributes[0] as DescriptionAttribute).Description;
                Parameters.Add(parameter.Name.ToUpper(CultureInfo.CurrentCulture), parameter);
            }
            foreach (var param in Parameters)
                if (param.Value.RequiredParameters.Count>0)
                    foreach (var required in param.Value.RequiredParameters)
                        if (!Parameters.ContainsKey(required.ToUpper(CultureInfo.CurrentCulture)))
                            Error += param.Key + ": The required parameter '" + required + "' is no valid parameter." + System.Environment.NewLine;
            if (String.IsNullOrEmpty(Error))
                return true;
            Parameters.Clear();
            return false;
        }

        #endregion
    }
}